<?php

namespace s94\wechat;

use Exception;

/**
 * 微信支付
 */
class Pay extends Base
{
    protected $encrypt_certificate_no = null;

    /**根据证书内容，获取其序列号
     * @param string $cert_content 证书内容
     * @return mixed
     */
    public static function getCertificateNo(string $cert_content){
        $cert_data = openssl_x509_parse($cert_content);
        self::assert($cert_data, '解析失败，请检查传入对象是否为证书');
        $no = $cert_data['serialNumberHex'];
        unset($cert_data);
        return $no;
    }

    /**保存商户私钥
     * @param string $key_content 私钥内容
     * @return mixed
     */
    public function saveCertificate(string $key_content, $certificate_no=null){
        $certificate_no = $this->config('certificate_no',$certificate_no);
        self::assert($certificate_no, '证书编号不能为空');
        $path = $this->config('cache_dir').$certificate_no.'_key.pem';
        file_put_contents($path, $key_content);
        return $key_content;
    }
    /**获取证书
     * @param string $no 证书序列号
     * @param mixed $key 为true表示获取私钥
     * @return false|string
     * @throws Exception
     */
    protected function getCertificate(string $no, $key=false){
        $name = $no.'_'.($key?'key':'cert').'.pem';
        $path = $this->config('cache_dir').$name;
        self::assert(is_file($path),($key?'商户私钥':'微信平台证书')."【{$name}】不存在".($key?'，请先上传商户私钥到缓存目录':''));
        return file_get_contents($path);
    }
    /**V3支付版本的签名
     * @param array $data 参与签名的参数
     * @return string
     * @throws Exception
     */
    protected function sign(array $data)
    {
        $message = implode("\n", $data)."\n";
        $no = $this->config('certificate_no');
        $private_key = $this->getCertificate($no, true);
        self::assert(openssl_sign($message, $signature, $private_key, OPENSSL_ALGO_SHA256),"签名失败，请检查商户密钥是否正确");
        return base64_encode($signature);
    }

    /**使用微信公钥对敏感信息进行加密
     * @param string $str 待加密的字符
     * @return string
     * @throws Exception
     */
    protected function encryptStr($str)
    {
        if ($this->encrypt_certificate_no){
            $public_key = $this->getCertificate($this->encrypt_certificate_no);
        }else{
            $list = $this->getWechatCertificate();
            foreach ($list as $no=>$content){
                $this->encrypt_certificate_no = $no;
                $public_key = $content;
                break;
            }
        }
        self::assert(openssl_public_encrypt($str, $encrypted, $public_key, OPENSSL_PKCS1_OAEP_PADDING), '敏感信息加密失败');
        return base64_encode($encrypted);
    }

    /**使用商户私钥对敏感信息解密
     * @param string $str 待解密的字符
     * @return string
     * @throws Exception
     */
    protected function decryptStr($str)
    {
        $no = $this->config('certificate_no');
        $private_key = $this->getCertificate($no, true);
        self::assert(openssl_private_decrypt(base64_decode($str), $decrypted, $private_key, OPENSSL_PKCS1_OAEP_PADDING), '敏感信息解密失败');
        return $decrypted;
    }

    /**使用key对密文进行解密，应用场景：获取微信平台证书，支付回调消息
     * @param mixed $ciphertext 密文
     * @param mixed $nonce 随机字符串
     * @param mixed $associated_data 附加数据包
     * @return false|string
     * @throws \SodiumException
     */
    protected function decrypt($ciphertext, $nonce, $associated_data)
    {
        $key = $this->config('key');
        $AUTH_TAG_LENGTH = 16;
        $ciphertext = base64_decode($ciphertext);
        $res = false;
        if (strlen($ciphertext) > $AUTH_TAG_LENGTH){
            $ctext = substr($ciphertext, 0, -$AUTH_TAG_LENGTH);
            $auth_tag = substr($ciphertext, -$AUTH_TAG_LENGTH);
            $res = openssl_decrypt($ctext, 'aes-256-gcm', $key, \OPENSSL_RAW_DATA, $nonce, $auth_tag, $associated_data);
        }
        self::assert($res!==false, '数据解密失败，请检查配置【key】是否正确');
        return $res;
    }

    /**使用微信平台证书对签名进行验证，引用场景：api接口响应消息，支付回调消息
     * @param array $header http头消息
     * @param string $body http主体消息
     * @return void
     * @throws Exception
     */
    protected function verfify($header, $body)
    {
        self::assert(!empty($header['Wechatpay-Timestamp']) && !empty($header['Wechatpay-Nonce']) && !empty($header['Wechatpay-Serial']) && !empty($header['Wechatpay-Signature']),'缺少签名验证数据');

        $timestamp = $header['Wechatpay-Timestamp'];
        $nonce = $header['Wechatpay-Nonce'];
        $wechatpay_no = $header['Wechatpay-Serial'];//微信支付平台公钥
        $signature = $header['Wechatpay-Signature'];

        $message = "{$timestamp}\n{$nonce}\n{$body}\n";
        $signature = base64_decode($signature);

        //微信支付平台公钥
        try {
            $pub_key = $this->getCertificate($wechatpay_no);
        } catch (Exception $e) {
            $this->getWechatCertificate();
            $pub_key = $this->getCertificate($wechatpay_no);
        }
        //验证签名
        $ok = openssl_verify($message, $signature, $pub_key, OPENSSL_ALGO_SHA256);
        self::assert($ok == 1,$ok ? openssl_error_string() : "响应验证失败");
    }

    public function apiPay($api, array $post_data=[], $skip_verfify=false)
    {
        $path = preg_match("/^\//",$api) ? $api : '/'.$api;
        $url = 'https://api.mch.weixin.qq.com'.$path;
        $body = $post_data ? json_encode($post_data) : '';
        $type = $body ? 'POST' : 'GET';
        $timestamp = time();
        $nonce = self::randomStr();
        $data = [$type, $path, $timestamp, $nonce, $body];
        $signature = $this->sign($data);

        $authorization = sprintf('mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
            $this->config('mchid'), $nonce, $timestamp, $this->config('certificate_no'), $signature);
        $header = [
            'Content-Type: application/json',
            'Accept: application/json',
            'User-Agent: '.$this->config('user_agent','s94_wechat_pay'),
            'Authorization: WECHATPAY2-SHA256-RSA2048 '.$authorization,
        ];
        if ($this->encrypt_certificate_no) $header[] = 'Wechatpay-Serial: '.$this->encrypt_certificate_no;
        $res = self::curl($url, $header, $body?:null);
        $this->log(['url'=>$url,'config'=>$this->config,'request'=>['header'=>$header,'body'=>$body],'response'=>$res]);
        $data = json_decode($res['body'], true);
        $err = $res['code']>=400;
        self::assert(!$err, $data['message'] ?? $res['body']);
        //验证响应消息
        if(!$skip_verfify) $this->verfify($res['header'], $res['body']);
        //返回响应
        return $data;
    }

    public function jsapiConfig($prepay_id, $appid=null)
    {
        $config = [
            'appId' => $appid ?: $this->config('appid'),
            'timeStamp' => time(),
            'nonceStr' => self::randomStr(),
            'package' => 'prepay_id='.$prepay_id,
            'signType' => 'RSA',
        ];
        $config['paySign'] = $this->sign([$config['appId'], $config['timeStamp'], $config['nonceStr'], $config['package'] ]);
        return $config;
    }

    public function appConfig($prepay_id)
    {
        $config = [
            'appid' => $this->config('appid'),
            'partnerid' => $this->config('mchid'),
            'prepayid' => $prepay_id,
            'package' => 'Sign=WXPay',
            'timestamp' => time(),
            'nonceStr' => self::randomStr(),
        ];
        $config['paySign'] = $this->sign([$this->config('appid'), $config['timestamp'], $config['nonceStr'], $config['prepayid'] ]);
        return $config;
    }

    /**支付、退款结果通知内容解密和解析
     * @return array 解析结果，格式：['header'=>头消息, 'body'=>主体数据, 'event_type'=>通知类型等同于$body['event_type'], 'data'=>解密后的通知数据]
     * @throws \SodiumException
     */
    public function callback()
    {
        $body = file_get_contents('php://input');
        //验证请求消息
        $map = ['Wechatpay-Timestamp','Wechatpay-Nonce','Wechatpay-Serial','Wechatpay-Signature'];
        $header = [];
        foreach ($map as $k){
            $kk = 'HTTP_'.str_replace('-','_',strtoupper($k));
            $header[$k] = $_SERVER[$kk] ?? '';
        }
        $this->log(['url'=>($_SERVER['HTTP_HOST']??'').($_SERVER['REQUEST_URI']??''),'config'=>$this->config,'request'=>['header'=>$header,'body'=>$body]]);
        $this->verfify($header, $body);
        $body = json_decode($body, true);

        $ciphertext = $body['resource']['ciphertext'];
        $nonce = $body['resource']['nonce'];
        $associated_data = $body['resource']['associated_data'];
        $content = $this->decrypt($ciphertext, $nonce, $associated_data);
        $data = json_decode($content, true);
        return [
            'header' => $header,
            'body' => $body,
            'event_type' => $body['event_type'],
            'data' => $data,
        ];
    }
    /**获取微信平台证书
     * @return array
     * @throws Exception
     */
    public function getWechatCertificate()
    {
        $res = $this->apiPay('v3/certificates', [], true);
        $list = $res['data'];
        $res = [];
        foreach ($list as $row){
            $ciphertext = $row['encrypt_certificate']['ciphertext'];
            $nonce = $row['encrypt_certificate']['nonce'];
            $associated_data = $row['encrypt_certificate']['associated_data'];
            $content = $this->decrypt($ciphertext, $nonce, $associated_data);
            $path = $this->config('cache_dir').$row['serial_no'].'_cert.pem';
            file_put_contents($path, $content);
            $res[$row['serial_no']] = $content;
        }
        return $res;
    }

    /**通用支付接口
     * @param string $type 支付类型：app，h5，native，jsapi
     * @param string $out_trade_no 商户单号
     * @param int $money 支付金额，单位：分
     * @param string $description 商品描述
     * @param array $extend 扩展数据，格式参考微信官方文档
     * @return array
     * @throws Exception
     */
    public function pay($type, $out_trade_no, $money, $description, $extend=[])
    {
        self::assert(in_array($type, ['app','h5','native','jsapi']),"支付类型错误");
        $post_data = [
            'appid' => $this->config('appid'),
            'mchid' => $this->config('mchid'),
            'description' => $description,
            'out_trade_no' => $out_trade_no,
            'notify_url' => $this->config('notify_url'),
            'amount' => [
                'total' => (int)$money,
                'currency' => 'CNY'
            ],
        ];
        switch ($type){
            case 'jsapi':{
                self::assert(!empty($extend['payer']) && !empty($extend['payer']['openid']),"缺少openid");
            }break;
            case 'h5':{
                self::assert(!empty($extend['scene_info']) && !empty($extend['scene_info']['payer_client_ip']),"缺少payer_client_ip");
                if (!isset($extend['scene_info']['h5_info'])) $extend['scene_info']['h5_info'] = [];
                if (!isset($extend['scene_info']['h5_info']['type'])) $extend['scene_info']['h5_info']['type'] = 'Wap';
            }break;
        }
        if ($extend) $post_data = array_merge_recursive($post_data, $extend);
        $res = $this->apiPay('v3/pay/transactions/'.$type, $post_data);
        return $res;
    }

    /**jsapi支付
     * @param string $out_trade_no 商户单号
     * @param int $money 支付金额，单位：分
     * @param string $description 商品描述
     * @param string $openid 支付者openid，可以添加“sub:”前缀来表示子商户对应的openid，例如：sub:oUpF8uMuAJO_M2pxb1Q9zNjWeS6o
     * @param array $extend 扩展数据，格式参考微信官方文档
     * @return array 带有paySign的配置参数
     * @throws Exception
     */
    public function jsapi($out_trade_no, $money, $description, $openid, $extend=[])
    {
        if ($extend) {
            $extend = array_merge_recursive($extend, ['payer'=> ['openid'=>$openid]]);
        }else{
            $extend = ['payer'=> ['openid'=>$openid]];
        }
        $res = $this->pay('jsapi', $out_trade_no, $money, $description, $extend);
        return $this->jsapiConfig($res['prepay_id']);
    }
    /**h5支付
     * @param string $out_trade_no 商户单号
     * @param int $money 支付金额，单位：分
     * @param string $description 商品描述
     * @param string $ip 支付客户端的IP地址
     * @param array $extend 扩展数据，格式参考微信官方文档
     * @return string h5_url，支付跳转链接地址
     * @throws Exception
     */
    public function h5($out_trade_no, $money, $description, $ip, $extend=[])
    {
        if ($extend) {
            $extend = array_merge_recursive($extend, ['scene_info'=> ['payer_client_ip'=>$ip]]);
        }else{
            $extend = ['scene_info'=> ['payer_client_ip'=>$ip]];
        }
        return $this->pay('h5', $out_trade_no, $money, $description, $extend)['h5_url'];
    }
    /**native支付
     * @param string $out_trade_no 商户单号
     * @param int $money 支付金额，单位：分
     * @param string $description 商品描述
     * @param array $extend 扩展数据，格式参考微信官方文档
     * @return string code_url，用于生成支付二维码
     * @throws Exception
     */
    public function native($out_trade_no, $money, $description, $extend=[])
    {
        return $this->pay('native', $out_trade_no, $money, $description, $extend)['code_url'];
    }
    /**app支付
     * @param string $out_trade_no 商户单号
     * @param int $money 支付金额，单位：分
     * @param string $description 商品描述
     * @param array $extend 扩展数据，格式参考微信官方文档
     * @return array 带有paySign的配置参数
     * @throws Exception
     */
    public function app($out_trade_no, $money, $description, $extend=[])
    {
        $res = $this->pay('app', $out_trade_no, $money, $description, $extend);
        return $this->appConfig($res['prepay_id']);
    }

    /**查询支付单详情
     * @param string $out_trade_no 商户单号
     * @param string $transaction_id 微信支付订单号，和$out_trade_no二选一
     * @return mixed
     * @throws Exception
     */
    public function info($out_trade_no, $transaction_id=null)
    {
        self::assert($out_trade_no || $transaction_id, '缺少参数');
        $query = ['mchid'=> $this->config('mchid')];
        if ($out_trade_no){
            $api = 'v3/pay/transactions/out-trade-no/'.$out_trade_no;
        }elseif ($transaction_id){
            $api = 'v3/pay/transactions/id/'.$transaction_id;
        }
        $api .= '?'.http_build_query($query);
        $res = $this->apiPay($api);
        return $res;
    }

    /**关闭订单
     * @param string $out_trade_no 商户单号
     * @return null
     */
    public function close($out_trade_no)
    {
        $post_data = [
            'mchid' => $this->config('mchid'),
        ];
        $res = $this->apiPay('v3/pay/transactions/out-trade-no/'.$out_trade_no.'/close', $post_data);
        return $res;
    }

    /**退款
     * @param string $out_trade_no 退款的支付单的商户单号
     * @param string $out_refund_no 退款单号
     * @param int $money 退款金额，单位：分
     * @param int $total 订单总金额，单位：分
     * @param string $reason 退款原因
     * @param array $extend 扩展数据，格式参考微信官方文档
     * @return array 退款返回数据，具体参考微信官方文档
     */
    public function refund($out_trade_no, $out_refund_no, $money, $total, $reason='', $extend=[])
    {
        $post_data = [
            'out_trade_no' => $out_trade_no,
            'out_refund_no' => $out_refund_no,
            'reason' => $reason,
            'notify_url' => $this->config('notify_url'),
            'amount' => [
                'refund' => (int)$money,
                'total' => (int)$total,
                'currency' => 'CNY'
            ],
        ];
        if ($extend) $post_data = array_merge_recursive($post_data, $extend);
        $res = $this->apiPay('v3/refund/domestic/refunds', $post_data);
        return $res;
    }

    /**退款详情
     * @param string $out_refund_no 退款单号
     * @return mixed
     */
    public function refundInfo($out_refund_no)
    {
        $res = $this->apiPay('v3/refund/domestic/refunds/'.$out_refund_no);
        return $res;
    }

    /**商家转账
     * @param string $out_bill_no 商家单号
     * @param string $openid 用户openid
     * @param int $money 转账金额，单位:分
     * @param string $scene_id 使用的转账场景，可前往“商户平台-产品中心-商家转账”中申请
     * @param array $transfer_scene_report_infos 转账场景报备信息，不同场景需要的info_type不同，每一项必须必须包含info_type、info_content
     * @param string $remark 转账备注，最多允许32个字符
     * @param string $user_name 收款方真实姓名，转账金额达到2000时必填
     * @return mixed
     * @throws Exception
     */
    public function mchTransfer($out_bill_no, $openid, $money, $scene_id, $transfer_scene_report_infos, $remark, $user_name=null)
    {
        $transfer_scene_id = $scene_id;
        $post_data = [
            'appid' => $this->config('appid'),
            'out_bill_no' => $out_bill_no,
            'transfer_scene_id' => (string)$scene_id,
            'openid' => $openid,
            'transfer_amount' => (int)$money,
            'transfer_remark' => $remark,
            'notify_url' => $this->config('notify_url'),
            'transfer_scene_report_infos'=> $transfer_scene_report_infos,
        ];
        if ($user_name){
            $post_data['user_name'] = $this->encryptStr($user_name);
        }elseif($money >= 200000){
            throw new \Exception('转账金额大于等于2000元，收款用户姓名必填');
        }
        $res = $this->apiPay('v3/fund-app/mch-transfer/transfer-bills', $post_data);
        return $res;
    }
}
